using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Threading;
using System.Xml;
using System.Reflection;

namespace CodePlex.Resourceful
{

    public class Client
    {

        private static readonly string DefaultUserAgent;

        static Client() 
        {

#if SILVERLIGHT
            Client.DefaultUserAgent = string.Format("{0} {1} (http://www.codeplex.com/resourceful)",
                typeof(Client).FullName,
                "0.0.1 Silverlight"); 
#else
            ServicePointManager.Expect100Continue = true;      // otherwise: An unexpected error occurred on a receive.
            

            Client.DefaultUserAgent = string.Format("{0} {1} (http://www.codeplex.com/resourceful)",
                typeof(Client).FullName,
                typeof(Client).Assembly.GetName().Version.ToString(3));

            // TODO this should be configurable
            // also per service-point?
            ServicePointManager.DefaultConnectionLimit = int.MaxValue;

#endif
        }


        
        private Uri _rootUri;
        private readonly Tracer _tracer;
        private string _userAgent;
        private TimeSpan _throttle;
        private DateTime _lastRequestTime;
        private NameValuePairs _commonHeaders = new NameValuePairs();
        
       
        public Client(Uri rootUri)
        {
            _userAgent = DefaultUserAgent;
            this.RootUri = rootUri;

            _tracer = new Tracer();
        }


        public Uri RootUri
        {
            get
            {
                return _rootUri;
            }
            set
            {
                _rootUri = value;
            }
        }

        public NameValuePairs CommonHeaders
        {
            get
            {
                return _commonHeaders;
            }
        }


        public Tracer Tracer
        {
            get
            {
                return _tracer;
            }
        }


        public string UserAgent
        {
            get
            {
                return _userAgent;
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    value = DefaultUserAgent;
                }
                _userAgent = value;
            }
        }

        public TimeSpan Throttle
        {
            get
            {
                return _throttle;
            }
            set
            {
                _throttle = value;
            }
        }




#if SILVERLIGHT
        private static IEnumerable<string> All(WebHeaderCollection headers)
        {
            return headers.Headers;
        }
#else

        private readonly CredentialCache _credentials = new CredentialCache();  // does not exist on SL

        private static IEnumerable<string> All(WebHeaderCollection headers)
        {
            return headers.AllKeys;
        }

        
        public void SetBasicAuth(string path, string name, string password)
        {
            Uri prefix = new Uri(this.RootUri + path);
            string type = "Basic";

            _credentials.Remove(prefix, type);  // just in case
            _credentials.Add(prefix, type, new NetworkCredential(name, password));
        }
#endif





        // get

        public ClientResponse GetRepresentation(string path, NameValuePairs parameters)
        {
            return this.GetRepresentation(Utils.EncodeUrlPath(path, parameters));
        }

        public ClientResponse GetRepresentation(string path)
        {
            return this.DoRequest(HttpMethod.Get, path, null, null,null);
        }

        public ClientResponse GetRepresentation(string path, string mediaType)
        {
            return this.DoRequest(HttpMethod.Get, path, mediaType, null, null);
        }
        public ClientResponse GetRepresentation(string path, string mediaType, NameValuePairs parameters)
        {
            return this.GetRepresentation(Utils.EncodeUrlPath(path, parameters), mediaType);
        }
     
        public ClientResponse GetRepresentation(string path, string mediaType, DateTime ifModifiedSince)
        {
            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.IfModifiedSince] = Utils.ToHttpDate(ifModifiedSince);

            return this.DoRequest(HttpMethod.Get, path,mediaType, headers, null);
        }



        // head

        public ClientResponse HeadRepresentation(string path)
        {
            return this.DoRequest(HttpMethod.Head, path, null, null, null);
        }

        public ClientResponse HeadRepresentation(string path, NameValuePairs parameters)
        {
            return this.HeadRepresentation(Utils.EncodeUrlPath(path, parameters));
        }



        // post

        public ClientResponse PostRepresentation(string path, string mediaType, string representation)
        {
            Stream input = new MemoryStream(Encoding.UTF8.GetBytes(representation));
            return this.PostRepresentation(path, mediaType, input);
        }
        public ClientResponse PostRepresentation(string path, string mediaType, Stream representation)
        {
            return this.DoRequest(HttpMethod.Post, path, mediaType, null,representation);
        }
        public ClientResponse PostRepresentation(string path, string mediaType, NameValuePairs parameters)
        {
            return this.PostRepresentation(path, mediaType, Utils.EncodeUrlPath(parameters));
        }



        // put 

        public ClientResponse PutRepresentation(string path, string mediaType, string representation)
        {
            Stream input = new MemoryStream(Encoding.UTF8.GetBytes(representation));

            return this.PutRepresentation(path, mediaType, input);
           
        }
        public ClientResponse PutRepresentation(string path, string mediaType, Stream representation)
        {
            return this.DoRequest(HttpMethod.Put, path, mediaType, null,representation);
        }

        public ClientResponse PutRepresentation(string path, string mediaType, NameValuePairs parameters)
        {
            return this.PutRepresentation(path, mediaType, Utils.EncodeUrlPath(parameters));
        }


        // delete

        public ClientResponse DeleteRepresentation(string path)
        {
            return this.DoRequest(HttpMethod.Delete, path, null, null, null);
        }
        public ClientResponse DeleteRepresentation(string path, NameValuePairs parameters)
        {
            return this.DeleteRepresentation(Utils.EncodeUrlPath(path, parameters));
        }






      

































        private void TraceResponseHeaders(HttpWebResponse response)
        {
            _tracer.WriteLine();
            _tracer.WriteLine("{0} {1}", (int)response.StatusCode, response.StatusDescription);

            foreach (string header in All(response.Headers))
            {
                _tracer.WriteLine("{0}:{1}", header, response.Headers[header]);
            }
        }


        private void TraceRequestHeaders(HttpWebRequest request, bool setContentLength)
        {

            _tracer.WriteLine();

#if SILVERLIGHT
            _tracer.WriteLine("{0} {1}", request.Method, Utils.GetPathAndQuery(request.RequestUri));
#else
            _tracer.WriteLine("{0} {1} HTTP/{2}", request.Method, Utils.GetPathAndQuery(request.RequestUri), request.ProtocolVersion.ToString(2));
#endif

            _tracer.WriteLine("{0}:{1}", HttpHeader.Host, request.RequestUri.Host);
            if (setContentLength)
            {
                _tracer.WriteLine("{0}:{1}", HttpHeader.ContentLength, request.ContentLength);
            }

            foreach (string header in All(request.Headers))
            {
                _tracer.WriteLine("{0}:{1}", header, request.Headers[header]);
            }
            
        }




        private ClientResponse DoRequest(string method, string path, string mediaType, NameValuePairs headers, Stream input)
        {
            headers = headers == null ? new NameValuePairs() : headers;

            if (mediaType != null)
            {
                if (Utils.In(method, HttpMethod.Post, HttpMethod.Put))
                {
                    headers[HttpHeader.ContentType] = mediaType;
                }
                else
                {
                    headers[HttpHeader.Accept] = mediaType;
                }
            }

            return this.DoRequest(method, path, headers, input,null,null,true);
        }




        protected ClientResponse DoRequest(
            string method, string path, NameValuePairs headers, Stream input,
            Action<long> bytesIn, Action<long> bytesOut, bool bufferOutput)
        {

            if (path.StartsWith(this.RootUri.ToString())) 
            {
                path = path.Substring(this.RootUri.ToString().Length);
            }
         
            while (this.Throttle != TimeSpan.Zero && DateTime.Now < _lastRequestTime + this.Throttle )
            {
                Thread.Sleep(10);
            }


            headers = headers == null ? new NameValuePairs() : headers;
           
#if SILVERLIGHT
            HttpWebRequest request = new System.Windows.Browser.Net.BrowserHttpWebRequest(new Uri(this.RootUri.ToString() + path));
            request.Headers[HttpHeader.UserAgent] = this.UserAgent;
#else

            Uri requestUri = new Uri(this.RootUri +  path);
            

            HttpWebRequest request = HttpWebRequest.Create(requestUri) as HttpWebRequest;

            // don't buffer the write stream if content-length is provided
            if (headers.ContainsKey(HttpHeader.ContentLength))
            {
                request.AllowWriteStreamBuffering = false;
            }


            // TODO these two should be configurable
            request.ReadWriteTimeout = Timeout.Infinite;
            request.Timeout = Timeout.Infinite;


            request.Credentials = _credentials;
            request.AllowAutoRedirect = false;
            request.UserAgent = this.UserAgent;
#endif


            request.Method = method;

            foreach (string header in this.CommonHeaders.Keys)
            {
                headers[header] = this.CommonHeaders[header];
            }


            // set request headers
            string hostHeader = null;
            bool setContentLength = false;
            foreach (string header in headers.Keys)
            {
                if (header == HttpHeader.Accept)
                {
                    request.Accept = headers[header];
                }
                else if (header == HttpHeader.ContentType)
                {
                    request.ContentType = headers[header];
                }
                
#if SILVERLIGHT
             
#else
                else if (header == HttpHeader.IfModifiedSince)
                {
                    request.IfModifiedSince = Utils.FromHttpDate(headers[header]);
                }
                else if (header == HttpHeader.ContentLength)
                {
                    request.ContentLength = long.Parse(headers[header]);
                    setContentLength = true;
                }
#endif
                else if (header == HttpHeader.Host)
                {
                    hostHeader = headers[header];
                }
               
                else
                {
                    request.Headers[header] = headers[header];
                }
            }

            // apply hacks to the request, if necessary
            UriHacker uriHacker = new UriHacker(request, hostHeader);



            // trace the request headers
            bool hadListeners = _tracer.HasListeners;
            if (hadListeners)
            {
                _tracer.WriteLine();
                this.TraceRequestHeaders(request, setContentLength);
            }
            

            // write to the request stream
            if (input != null && method != HttpMethod.Head)
            {
                Stream requestStream = request.GetRequestStream();
                if (hadListeners)
                {
                    requestStream = new MonitoredStream(requestStream, _tracer.Write);
                }

                if (EnvironmentInfo.IsSilverlight) 
                {
                    Utils.BufferedTransfer(input, requestStream, bytesIn);
                    requestStream.Flush();
                } 
                else
                {
                    using (requestStream)
                    {
                        Utils.BufferedTransfer(input, requestStream, bytesIn);
                    }
                }
            }

            
            // send the request, get the response
            HttpWebResponse response = null;
            WebException webex = null;
            try
            {
                using (uriHacker)
                {
                    response = request.GetResponse() as HttpWebResponse;
                }
            }
            catch (WebException ex)
            {
                webex = ex;
                response = ex.Response as HttpWebResponse;  // response buried in the exception
            }

            _lastRequestTime = DateTime.Now;

            if (response == null)
            {
                throw webex;
            }

            // trace the response headers
            if (hadListeners)
            {
                this.TraceResponseHeaders(response);
            }

            // read status and headers
            HttpStatus status = HttpStatus.FromCode(response.StatusCode);
            NameValuePairs responseHeaders = NameValuePairs.From(response.Headers);

            if (EnvironmentInfo.IsMono && status.Equals(HttpStatus.NoContent))
            {
                // don't even try to get the response stream (mono won't give you one...)
                return new ClientResponse(status, responseHeaders, Utils.ToStream(string.Empty));
            }


            // handle the response stream
            Stream responseStream = null;

            if (hadListeners)
            {
                responseStream = new MonitoredStream(response.GetResponseStream(), _tracer.Write);
            }
            else
            {
                responseStream = response.GetResponseStream();
            }

           
            // read the response
            if (bufferOutput)
            {
                // buffer: read entire stream up front (and close the response stream)
                MemoryStream ms = new MemoryStream();
             
                Utils.BufferedTransfer(responseStream, ms, bytesOut);
                
                ms.Position = 0;
                response.Close();
               
                return new ClientResponse(status, responseHeaders, ms);
            }
            else
            {
                // don't buffer: the responsbility of closing the response stream is on the caller
                return new ClientResponse(status, responseHeaders, responseStream);
            }
        }

    }
}
